﻿Imports System.Text.RegularExpressions
Imports System.ComponentModel
Imports System.Net
Imports System.Windows.Threading
Imports System.Globalization
Imports <xmlns:nicolive="nicolive">


Public Class NicoLiveService
    Private Shared service As NicoLiveService = New NicoLiveService

    Private Sub New()
        CrawlingTimer = New DispatcherTimer
        CrawlingTimer.Interval = New TimeSpan(0, 0, 3)
    End Sub

    Public Shared Function GetInstance() As NicoLiveService
        Return service
    End Function

    Private LivePrograms As New List(Of LiveProgramModel)

#Region "Events"

    Public Event LiveProgramStarted As EventHandler(Of LiveProgramEventArgs)
    Protected Overridable Sub OnLiveProgramStarted(ByVal e As LiveProgramEventArgs)
        RaiseEvent LiveProgramStarted(Me, e)
    End Sub

    Public Event LiveProgramClosed As EventHandler(Of LiveProgramEventArgs)
    Protected Overridable Sub OnLiveProgramClosed(ByVal e As LiveProgramEventArgs)
        RaiseEvent LiveProgramClosed(Me, e)
    End Sub

    Public Event LiveProgramUpdated As EventHandler(Of LiveProgramEventArgs)
    Protected Overridable Sub OnLiveProgramUpdated(ByVal e As LiveProgramEventArgs)
        RaiseEvent LiveProgramUpdated(Me, e)
    End Sub

#End Region

    Private FirstPageCrawlingDateTime As DateTime
    Private AllPagesCrawlingDateTime As DateTime
    Private LastPageCrawlingDateTime As DateTime
    Private ProgramCrawlingDateTime As DateTime
    Private TimeTableCrawlingDateTime As DateTime

    Private WithEvents CrawlingTimer As DispatcherTimer

#Region "Properties"

    Private _scheduling As CrawlScheduling = CrawlScheduling.NoPages
    Public Property Scheduling() As CrawlScheduling
        Get
            Return _scheduling
        End Get
        Set(ByVal value As CrawlScheduling)
            _scheduling = value
            If _scheduling = CrawlScheduling.NoPages OrElse _scheduling = CrawlScheduling.None Then
                CrawlingTimer.Stop()
            Else
                CrawlingTimer.Start()
            End If
        End Set
    End Property

    Private _interval As CrawlInterval = CrawlInterval.Normal
    Public Property Interval() As CrawlInterval
        Get
            Return _interval
        End Get
        Set(ByVal value As CrawlInterval)
            _interval = value
        End Set
    End Property

    Private _detailCrawlingEnabled As Boolean = True
    Public Property DetailCrawlingEnabled() As Boolean
        Get
            Return _detailCrawlingEnabled
        End Get
        Set(ByVal value As Boolean)
            _detailCrawlingEnabled = value
        End Set
    End Property

#End Region


    Private HighPriorityId As Integer
    Public Sub SetHighPriorityId(ByVal id As Integer)
        HighPriorityId = id
    End Sub


    Private Sub CrawlingTimer_Tick(ByVal sender As Object, ByVal e As System.EventArgs) Handles CrawlingTimer.Tick
        CrawlingTimer.Stop()

        If Me.Scheduling = CrawlScheduling.NoPages Then
            Exit Sub
        End If

        ' 全ページクロール
        If Me.Scheduling = CrawlScheduling.AllPages Then
            Dim interval As Integer
            Select Case Me.Interval
                Case CrawlInterval.Normal
                    interval = 3
                Case CrawlInterval.Long
                    interval = 6
                Case Else
                    interval = 6
            End Select

            If AllPagesCrawlingDateTime.AddMinutes(interval) < Now Then
                CrawlLivePrograms(CrawlScheduling.AllPages)
                Exit Sub
            End If
        End If

        ' 先頭ページのみクロール
        If (Me.Scheduling = CrawlScheduling.AllPages OrElse _
            Me.Scheduling = CrawlScheduling.FirstPageOnly) Then

            Dim interval As Integer
            Select Case Me.Interval
                Case CrawlInterval.Normal
                    interval = 30
                Case CrawlInterval.Long
                    interval = 60
                Case Else
                    interval = 60
            End Select

            If FirstPageCrawlingDateTime.AddSeconds(interval) < Now Then
                CrawlLivePrograms(CrawlScheduling.FirstPageOnly)
                Exit Sub
            End If
        End If

        ' 詳細ページクロール
        If Me.DetailCrawlingEnabled AndAlso ProgramCrawlingDateTime.AddSeconds(3) < Now Then

            ' 取得優先すべき放送
            Dim highPriorityPrograms = From p In LivePrograms Where Not p.HasFullContent AndAlso p.Id = HighPriorityId
            If highPriorityPrograms.Count > 0 Then
                CrawlLiveProgram(highPriorityPrograms.First.Id)
            Else
                Dim programs = From p In LivePrograms Where Not p.HasFullContent Order By p.StartDateTime Descending
                If programs.Count > 0 Then
                    CrawlLiveProgram(programs.First.Id)
                End If
            End If
        End If

        CrawlingTimer.Start()
    End Sub

#Region "Program Getting"

    Private WithEvents CrawlingLiveProgramWorker As BackgroundWorker


    Private Sub CrawlLiveProgram(ByVal id As Integer)

#If DEBUG Then
        Console.WriteLine(Now.ToString & " " & id.ToString)
#End If

        CrawlingLiveProgramWorker = New BackgroundWorker
        CrawlingLiveProgramWorker.WorkerReportsProgress = True
        CrawlingLiveProgramWorker.WorkerSupportsCancellation = True
        CrawlingLiveProgramWorker.RunWorkerAsync(id)

    End Sub

    Private Sub CrawlingLiveProgramWorker_DoWork(ByVal sender As Object, ByVal e As System.ComponentModel.DoWorkEventArgs) Handles CrawlingLiveProgramWorker.DoWork

        Dim id = DirectCast(e.Argument, Integer)
        Dim responseDate As DateTime
        Dim program = RetrieveLiveProgram(id, responseDate)

        e.Result = program

    End Sub

    Public Shared Function RetrieveLiveProgram(ByVal id As Integer, ByRef responseDate As DateTime) As LiveProgramModel

        Dim htmlBody = GetContent(New Uri(String.Format(CultureInfo.InvariantCulture, _
                                                        My.Settings.LiveProgramUriFormat, "lv" & id.ToString)), responseDate)

        If htmlBody = "" Then
            Return Nothing
        End If

        Dim program = New LiveProgramModel With {.Id = id}

        Dim title As String = ""
        Dim content As String = ""

        If My.Settings.LiveProgramPage_ProgramRegEx <> "" Then
            ' TODO
        Else
            program.Title = htmlBody.MatchedText(My.Settings.LiveProgramPage_TitleRegEx, "title").DecodeValue.Replace(vbCrLf, " ").Replace(vbLf, "").Replace(vbCr, "")
            program.Content = htmlBody.MatchedText(My.Settings.LiveProgramPage_ContentRegEx, "content").DecodeValue
        End If

        'program.Caster = htmlBody.MatchedText(My.Settings.LiveProgramPage_CasterRegEx, "caster").DecodeValue.Replace(vbCrLf, " ").Replace(vbLf, "").Replace(vbCr, "")

        Return program

    End Function

    Private Sub CrawlingLiveProgramWorker_RunWorkerCompleted(ByVal sender As Object, ByVal e As System.ComponentModel.RunWorkerCompletedEventArgs) Handles CrawlingLiveProgramWorker.RunWorkerCompleted

        If Not e.Cancelled AndAlso e.Result IsNot Nothing Then
            Dim program = DirectCast(e.Result, LiveProgramModel)
            Dim target = From p In LivePrograms Where p.Id = program.Id
            If target.Count > 0 Then
                With target.SingleOrDefault
                    .Title = If(program.Title <> "", program.Title, .Title)
                    .Content = If(program.Content <> "", program.Content, .Content)
                    .Caster = program.Caster ' MEMO Nothing以外がセットされることでクロール済みか判断
                End With

                OnLiveProgramUpdated(New LiveProgramEventArgs(target.SingleOrDefault))
            End If
        End If

        CrawlingLiveProgramWorker.Dispose()
        CrawlingTimer.Start()
    End Sub


#End Region

#Region "Live Programs Crawling"
    Private RecentId As Integer
    Private SyncObject As New Object

    Private WithEvents CrawlingLiveProgramsWorker As BackgroundWorker


    Private Sub CrawlLivePrograms(ByVal scheduling As CrawlScheduling)

#If DEBUG Then
        Console.WriteLine(Now.ToString & " " & scheduling.ToString)
#End If

        CrawlingLiveProgramsWorker = New BackgroundWorker
        CrawlingLiveProgramsWorker.WorkerReportsProgress = True
        CrawlingLiveProgramsWorker.WorkerSupportsCancellation = True
        CrawlingLiveProgramsWorker.RunWorkerAsync(scheduling)

    End Sub

    Private Sub CrawlingWorker_DoWork(ByVal sender As Object, ByVal e As DoWorkEventArgs) Handles CrawlingLiveProgramsWorker.DoWork

        Dim scheduling = DirectCast(e.Argument, CrawlScheduling)
        Dim ids = New List(Of Integer)
        Dim hasError As Boolean = False

        Dim currentPage = 1
        Dim previousId As Integer
        SyncLock SyncObject
            previousId = RecentId
        End SyncLock

        Do
            Dim uri = New Uri(String.Format(CultureInfo.InstalledUICulture, My.Settings.LiveProgramsFeedUriFormat, currentPage))
            If Not uri.IsWellFormedOriginalString Then
                Exit Do
            End If

            Dim responseDate As DateTime
            Dim body = GetContent(uri, responseDate)

            If body = "" Then
                hasError = True
                Exit Do
            End If

            ' 生放送情報取得
            Dim result = New LiveProgramsResult With { _
                .CurrentPage = currentPage}

            ' ページ内生放送取得
            Dim programs = RetrieveLivePrograms(body, responseDate)
            For Each p In programs
                result.LivePrograms.Add(p)
                ids.Add(p.Id)
            Next
            CrawlingLiveProgramsWorker.ReportProgress(0, result)

            If programs.Count = 0 Then
                Exit Do
            ElseIf currentPage = 1 Then
                SyncLock SyncObject
                    RecentId = programs.First.Id ' 一番新しい放送のIdを記憶
                End SyncLock
            End If

            Select Case scheduling
                Case CrawlScheduling.AllPages
                    currentPage += 1
                Case Else
                    Dim ps = From p In programs Where p.Id = previousId
                    If ps.Count > 0 Then
                        ' 現在取得した放送に前回取得の最新IDがある
                        Exit Do
                    ElseIf currentPage <= 3 Then
                        currentPage += 1
                    Else
                        Exit Do
                    End If
            End Select

            System.Threading.Thread.Sleep(50)

        Loop

        Dim crawlingResult = New LiveProgramsCrawlingResult With { _
            .HasError = hasError, _
            .Scheduling = scheduling}

        crawlingResult.Ids.AddRange(ids)
        e.Result = crawlingResult

    End Sub

    Private Shared Function RetrieveLivePrograms(ByVal body As String, ByVal responseDate As DateTime?) As IList(Of LiveProgramModel)

        Dim list = New List(Of LiveProgramModel)

        Try
            Dim doc = XDocument.Parse(body)

            For Each item In doc...<item>

                Dim content = item.<description>.Value.Replace(vbCrLf, " ").Replace(vbLf, "").Replace(vbCr, "")
                Dim title = item.<title>.Value.Replace(vbCrLf, " ").Replace(vbLf, "").Replace(vbCr, "")

                Dim isFace As Boolean = False
                Dim category As String = ""
                For Each c In item...<category>
                    If c.Value = "顔出し" Then
                        isFace = True
                    Else
                        category = c.Value
                    End If
                Next

                Dim station = New StationModel With { _
                    .Id = item.<nicolive:community_id>.Value, _
                    .Name = item.<nicolive:community_name>.Value}

                Dim program = New LiveProgramModel With { _
                    .Id = CInt(item.<guid>.Value.Substring(2)), _
                    .Title = title, _
                    .ShortContent = content, _
                    .StartDateTime = CDate(item.<pubDate>.Value), _
                    .VisitorCount = CInt(item.<nicolive:view>.Value), _
                    .CommentCount = CInt(item.<nicolive:num_res>.Value), _
                    .IsMemberOnly = CBool(item.<nicolive:member_only>.Value), _
                    .CrawlingDateTime = If(responseDate, Now), _
                    .IsFace = isFace, _
                    .OfficialTag = category, _
                    .Station = station}

                If item.<nicolive:owner_name>.Value IsNot Nothing Then
                    program.Caster = item.<nicolive:owner_name>.Value
                End If
                list.Add(program)
            Next

        Catch ex As Exception

        End Try

        Return list

    End Function

    Private Sub UpdateLivePrograms(ByVal result As LiveProgramsResult)

        For Each program In result.LivePrograms
            Dim pid = program.Id
            Dim existPrograms = From p In LivePrograms Where p.Id = pid
            If existPrograms.Count > 0 Then
                ' 情報の更新
                With existPrograms.SingleOrDefault
                    .VisitorCount = If(program.VisitorCount > .VisitorCount, program.VisitorCount, .VisitorCount)
                    .CommentCount = If(program.CommentCount > .CommentCount, program.CommentCount, .CommentCount)
                    .CrawlingDateTime = program.CrawlingDateTime
                End With

                OnLiveProgramUpdated(New LiveProgramEventArgs(existPrograms.SingleOrDefault))
            Else
                ' 新規生放送
                LivePrograms.Add(program)
                OnLiveProgramStarted(New LiveProgramEventArgs(program))
            End If
        Next

    End Sub

    Private Sub CrawlWorker_ProgressChanged(ByVal sender As Object, ByVal e As ProgressChangedEventArgs) Handles CrawlingLiveProgramsWorker.ProgressChanged

        Dim result = DirectCast(e.UserState, LiveProgramsResult)

        ' 番組追加・更新
        UpdateLivePrograms(result)

    End Sub

    Private Sub CrawlWorker_Completed(ByVal sender As Object, ByVal e As RunWorkerCompletedEventArgs) Handles CrawlingLiveProgramsWorker.RunWorkerCompleted
        CrawlingLiveProgramsWorker.Dispose()
        CrawlingTimer.Start()

        If e.Result Is Nothing Then
            Exit Sub
        End If

        Dim result = DirectCast(e.Result, LiveProgramsCrawlingResult)

        If Not e.Cancelled Then
            Select Case result.Scheduling
                Case CrawlScheduling.AllPages
                    AllPagesCrawlingDateTime = Now
                    FirstPageCrawlingDateTime = Now
                    LastPageCrawlingDateTime = Now

                Case CrawlScheduling.FirstPageOnly
                    FirstPageCrawlingDateTime = Now
            End Select
        Else
            Exit Sub
        End If

        If result.Ids.Count = 0 Then
            Exit Sub
        End If

        ' 番組削除
        If Not result.HasError AndAlso result.Scheduling = CrawlScheduling.AllPages Then
            For i = LivePrograms.Count - 1 To 0 Step -1
                If Not result.Ids.Contains(LivePrograms(i).Id) Then
                    Dim program = LivePrograms(i)
                    OnLiveProgramClosed(New LiveProgramEventArgs(program))
                    LivePrograms.Remove(program)
                End If
            Next
        End If


    End Sub



#End Region

    Private Shared Function GetContent(ByVal uri As Uri, ByRef [date] As DateTime) As String

        Dim request = DirectCast(WebRequest.Create(uri), HttpWebRequest)

        request.UserAgent = My.Settings.UserAgent

        If My.Settings.ProxyUserName <> "" OrElse _
           My.Settings.ProxyPassword <> "" Then
            request.Credentials = New NetworkCredential(My.Settings.ProxyUserName, My.Settings.ProxyPassword)
        End If

        Dim content As String = ""
        Try
            Dim response = DirectCast(request.GetResponse, HttpWebResponse)
            Using stream = response.GetResponseStream, _
                  reader = New System.IO.StreamReader(stream, System.Text.Encoding.UTF8)
                content = reader.ReadToEnd
            End Using

            Try
                If response.Headers(HttpResponseHeader.Date) IsNot Nothing Then
                    [date] = New DateTime(CDate(response.Headers(HttpResponseHeader.Date)).Ticks, DateTimeKind.Utc)
                Else
                    [date] = Now
                End If
            Catch ex As InvalidCastException
                [date] = Now
            End Try

        Catch webEx As WebException

        Catch ex As Exception

        End Try

        Return content
    End Function


End Class
